/*
 * Copyright 2015-2016 Amazon.com, Inc. or its affiliates.  All Rights Reserved.
 *
 * You may not use this file except in compliance with the terms and conditions set forth
 * in the accompanying LICENSE.TXT file.
 *
 * THESE MATERIALS ARE PROVIDED ON AN "AS IS" BASIS.  AMAZON SPECIFICALLY DISCLAIMS, WITH
 * RESPECT TO THESE MATERIALS, ALL WARRANTIES, EXPRESS, IMPLIED, OR STATUTORY, INCLUDING
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.
 */
package com.amazon.alexa.voicechrome;

import android.app.Activity;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.PopupWindow;

import com.amazon.alexa.voicechrome.internal.PopUpFactory;
import com.amazon.alexa.voicechrome.internal.audio.AudioUtil;
import com.amazon.alexa.voicechrome.internal.timer.SpeechUiTimer;
import com.amazon.alexa.voicechrome.internal.timer.TimerFactory;
import com.amazon.alexa.voicechrome.internal.timer.UpdateTimer;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * {@link VoiceChromeController VoiceChromeController} controls the state of a provided or implicitly created {@link VoiceChromeVisuals VoiceChromeVisuals} view.
 * <p>
 * The builder is provided for convenience. In order to initialize the voice chrome, {@link Builder#with(Activity) VoiceChromeController.Builder.with(Activity)} or one
 * of the other {@link Builder#with(Activity) "with()"} methods should be used.
 * </p>
 * <p>
 * A builder can also take a {@link VoiceChromeVisuals VoiceChromeVisuals} view. Use {@link Builder#with(VoiceChromeVisuals) VoiceChromeController.Builder.with(VoiceChromeVisuals)} to show the
 * voice chrome as part of the apps layout instead of on top as a window.
 * </p>
 *
 * For example:
 *

 <pre>
 {@code
     &#064;Override
     protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        VoiceChromeVisuals VoiceChromeVisuals = (VoiceChromeVisuals) findViewById(R.id.speech_ui_visuals);
        VoiceChromeController vcController = VoiceChromeController.Builder.with(VoiceChromeVisuals).setLoggingEnabled(true).build();
        vcController.doNotDisturb();
     }
 }
 </pre>

 */
public class VoiceChromeController implements UpdateTimer.TimerTickCallback, AudioUtil.OnRecordListener, PopupWindow.OnDismissListener, VoiceChromeVisuals.CloseClickedCallback {

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({LISTEN, SPEAK, THINK, DO_NOT_DISTURB, PRIVACY, UNAVAILABLE})
    public @interface STATE{}

    public static final int LISTEN = 0;
    public static final int SPEAK = 1;
    public static final int THINK = 2;
    public static final int DO_NOT_DISTURB = 3;
    public static final int PRIVACY = 4;
    public static final int UNAVAILABLE = 5;

    private PopupWindow popUp;
    private Handler handler;
    private TimerFactory timerFactory;
    private PopUpFactory popUpFactory;
    @Nullable
    private Activity activity;
    private VoiceChromeVisuals view;
    private boolean usePopUp;
    private AudioUtil audioUtil;
    private SpeechUiTimer timer;
    private boolean isDemo = false;

    private float volume;


    VoiceChromeController(@Nullable Activity activity, VoiceChromeVisuals view, final boolean usePopUp, AudioUtil audioUtil, Handler handler, TimerFactory timerFactory, PopUpFactory popUpFactory) {
        this.activity = activity;
        this.view = view;
        this.usePopUp = usePopUp;
        this.audioUtil = audioUtil;
        this.handler = handler;
        this.timerFactory = timerFactory;
        this.popUpFactory = popUpFactory;

        if (usePopUp) {
            createPopUp();
        }
    }

    /**
     * Hide the VoiceChromeVisuals and cancel recording if active
     */
    public void stop() {
        view.stop();
        closePopUpIfOpen();
        releaseRecordingAssets();
    }

    /**
     * Move the VoiceChromeVisuals to the listening state and begin listening to voice commands
     */
    public void listen() {
        view.setListening();
        commitStateChange(true);
    }

    /**
     * Move the VoiceChromeVisuals to the thinking state and stop recording. This should be called when
     * audio is being processed.
     */
    public void think() {
        view.setThinking();
        commitStateChange(false);
    }

    /**
     * Move the VoiceChromeVisuals to the speaking state and stop recording. This should be called when
     * audio has been processed and Alexa is responding.
     */
    public void speak() {
        view.setSpeaking();
        commitStateChange(false);
    }

    /**
     * Move the VoiceChromeVisuals to the privacy state to indicate that it is not listening.
     */
    public void privacy() {
        view.setPrivacy();
        commitStateChange(false);
    }

    /**
     * Move the VoiceChromeVisuals to the unavailable state to indicate that the voice service is not currently available.
     */
    public void unavailable() {
        view.setUnavailable();
        commitStateChange(false);
    }

    /**
     * Move the VoiceChromeVisuals to the do not disturb state.
     */
    public void doNotDisturb() {
        view.setDoNotDisturb();
        commitStateChange(false);
    }

    /**
     * Turns demo mode on or off allowing random amplitudes to be sent to the view without speaking
     * @param isDemo indicate if this should be a demo
     */
    public void setDemo(boolean isDemo) {
        this.isDemo = isDemo;
    }

    private void resetTimer() {
        destroyTimer();
        timer = timerFactory.create(this);
        timer.start();
    }

    private void destroyTimer() {
        if (timer != null) {
            timer.stop();
            timer = null;
        }
    }

    private void setVolume(float volume) {
        this.volume = volume;
    }

    private float getVolume() {
        return volume;
    }

    /**
     * Called on timer update events to update the UI with the current amplitude
     */
    @Override
    public void update() {
        handler.post(new Runnable() {
            @Override
            public void run() {
                if (isDemo) {
                    view.getListeningView().updateAnimation((float) Math.random());
                } else {
                    view.getListeningView().updateAnimation(getVolume());
                }
            }
        });
    }

    @Override
    public void onRelativeVolumeChanged(float volume) {
        setVolume(volume);
    }


    @Override
    public void onDismiss() {
        stop();
    }


    @Override
    public void onCloseClick() {
        stop();
    }

    /**
     * Transitions the UI to the selected state.
     * @param isListening if true, starts recording audio and update timer, otherwise it destroys the timer and stops recording
     */
    private void commitStateChange(boolean isListening) {
        if (isListening) {
            resetTimer();
            audioUtil.setOnRecordListener(this);
            audioUtil.start();
        } else {
            releaseRecordingAssets();
        }

        showPopupIfNeeded();
    }

    private void releaseRecordingAssets() {
        destroyTimer();
        audioUtil.stop();
        audioUtil.setOnRecordListener(null);
    }

    private void createPopUp() {
        popUp = popUpFactory.create(view);
        popUp.setBackgroundDrawable(null);
        // Needed so the overlay takes up the full screen when an app has hidden system bars
        popUp.setClippingEnabled(false);
        popUp.setAnimationStyle(R.style.voicechrome_popupWindowAnimationStyle);
        popUp.setOnDismissListener(this);

        // Used to make the overlay cancelable on touch
        // TODO: Should cancelable be publicly defined?
        popUp.setTouchable(true);
        view.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (usePopUp && popUp.isShowing()) {
                    popUp.dismiss();
                }
            }
        });
    }

    private void showPopUpAtLocation() {
        // Post the popup change so that the activity can finish its lifecycle events before we show the popup
        if (activity != null) {
            final View activityView = activity.findViewById(android.R.id.content);
            activityView.post(new Runnable() {
                @Override
                public void run() {
                    popUp.showAtLocation(activityView, Gravity.CENTER, 0, 0);
                }
            });
        } else {
            throw new IllegalArgumentException("No activity found to attach window to");
        }
    }

    private void showPopupIfNeeded() {
        if (usePopUp && !popUp.isShowing()) {
            showPopUpAtLocation();
        }
    }

    private void closePopUpIfOpen() {
        if (usePopUp && popUp.isShowing()) {
            popUp.dismiss();
        }
    }

    public static final class Builder {
        private VoiceChromeVisuals view;
        private boolean isDemo;
        private Activity activity;
        private boolean usePopUp = false;
        private boolean loggingEnabled = false;
        private AudioUtil audioUtil;
        private Handler handler;
        private PopUpFactory popupFactory;
        private TimerFactory timerFactory;

        Builder() {
        }

        /**
         * This will create the VoiceChromeVisuals in a window and attach the controller. Hiding and showing of the window will be handled automatically on start/stop respectively.
         * @param activity A {@link Activity fragment }to host the window
         * @return A {@link Builder Builder} for further customization.
         */
        public static Builder with(@NonNull Activity activity) {
            if (activity == null) {
                throw new IllegalArgumentException("activity can not be null");
            } else {
                return new Builder().withActivity(activity);
            }
        }

        /**
         * This will create the VoiceChromeVisuals in a window and attach the controller. Hiding and showing of the window will be handled automatically on start/stop respectively.
         * @param fragment A {@link android.app.Fragment fragment } to host the window
         * @return A {@link Builder Builder} for further customization.
         */
        public static Builder with(@NonNull android.app.Fragment fragment) {
            if (fragment == null) {
                throw new IllegalArgumentException("fragment can not be null");
            } else {
                Activity activity = fragment.getActivity();
                if (activity == null) {
                    throw new IllegalArgumentException("Fragment's activity can not be null");
                }
                return with(activity);
            }
        }

        /**
         * This will create the VoiceChromeVisuals in a window and attach the controller. Hiding and showing of the window will be handled automatically on start/stop respectively.
         * @param fragment A {@link android.support.v4.app.Fragment fragment } to host the window
         * @return A {@link Builder Builder} for further customization.
         */
        public static Builder with(@NonNull android.support.v4.app.Fragment fragment) {
            if (fragment == null) {
                throw new IllegalArgumentException("fragment can not be null");
            } else {
                Activity activity = fragment.getActivity();
                if (activity == null) {
                    throw new IllegalArgumentException("Fragment's activity can not be null");
                }
                return with(activity);
            }
        }

        /**
         *
         * @param voiceChromeVisuals a {@link VoiceChromeVisuals VoiceChromeVisuals} to control with this controller
         * @return A {@link Builder Builder} for further customization. The provided VoiceChromeVisuals should be in the view hierarchy and set as a full-screen view.
         */
        public static Builder with(@NonNull VoiceChromeVisuals voiceChromeVisuals) {
            if (voiceChromeVisuals == null) {
                throw new IllegalArgumentException("VoiceChromeVisuals can not be null");
            } else {
                return new Builder().withView(voiceChromeVisuals);
            }
        }

        Builder withView(VoiceChromeVisuals voiceChromeVisuals) {
            view = voiceChromeVisuals;
            return this;
        }

        Builder withActivity(Activity activity) {
            this.activity = activity;
            return this;
        }

        Builder withAudioUtil(AudioUtil audioUtil) {
            this.audioUtil = audioUtil;
            return this;
        }

        Builder withHandler(Handler handler) {
            this.handler = handler;
            return this;
        }

        Builder withTimerFactory(TimerFactory timerFactory) {
            this.timerFactory = timerFactory;
            return this;
        }

        Builder withPopupFactory(PopUpFactory popupFactory) {
            this.popupFactory = popupFactory;
            return this;
        }

        Builder withPopup(boolean usePopUp) {
            this.usePopUp = usePopUp;
            return this;
        }

        /**
         *
         * @param isDemo sets demo mode on or off. Demo mode emulators sound data
         * @return {@link Builder builder}
         */
        public Builder withDemo(boolean isDemo) {
            this.isDemo = isDemo;
            return this;
        }

        /**
         *
         * @param loggingEnabled turns debug logging on or off
         * @return {@link Builder builder}
         */
        public Builder setLoggingEnabled(boolean loggingEnabled) {
            this.loggingEnabled = loggingEnabled;
            return this;
        }

        /**
         *
         * @return a {@link VoiceChromeController VoiceChromeController}
         */
        public VoiceChromeController build() {
            setDefaults();
            VoiceChromeController controller = new VoiceChromeController(activity, view, usePopUp, audioUtil, handler, timerFactory, popupFactory);
            controller.setDemo(isDemo);
            view.setLoggingEnabled(loggingEnabled);
            view.setCloseClickCallback(controller);
            audioUtil.setLoggingEnabled(loggingEnabled);

            return controller;
        }

        private void setDefaults() {
            if (view == null) {
                this.usePopUp = true;
                view = new VoiceChromeVisuals(activity);
                view.setUseBackgroundOverlayIntroAnimations(usePopUp);
                view.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
            }

            if (audioUtil == null) {
                audioUtil = new AudioUtil();
            }

            if (handler == null) {
                handler = new Handler(Looper.getMainLooper());
            }

            if (timerFactory == null) {
                timerFactory = new TimerFactory();
            }

            if (popupFactory == null) {
                popupFactory = new PopUpFactory();
            }
        }
    }

}
